@lobehub/chat
Version:
Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.
317 lines (269 loc) • 10.3 kB
text/typescript
import { useAnalytics } from '@lobehub/analytics/react';
import { useCallback, useMemo } from 'react';
import { useGeminiChineseWarning } from '@/hooks/useGeminiChineseWarning';
import { getAgentStoreState } from '@/store/agent';
import { agentSelectors } from '@/store/agent/selectors';
import { getChatStoreState, useChatStore } from '@/store/chat';
import { aiChatSelectors, chatSelectors, topicSelectors } from '@/store/chat/selectors';
import { fileChatSelectors, useFileStore } from '@/store/file';
import { mentionSelectors, useMentionStore } from '@/store/mention';
import { useSessionStore } from '@/store/session';
import { sessionMetaSelectors } from '@/store/session/selectors';
import { getUserStoreState } from '@/store/user';
export interface UseSendMessageParams {
isWelcomeQuestion?: boolean;
onlyAddAIMessage?: boolean;
onlyAddUserMessage?: boolean;
}
export type UseSendGroupMessageParams = UseSendMessageParams & {
targetMemberId?: string;
};
export const useSend = () => {
const [
isContentEmpty,
sendMessage,
addAIMessage,
stopGenerateMessage,
cancelSendMessageInServer,
generating,
isSendButtonDisabledByMessage,
isSendingMessage,
] = useChatStore((s) => [
!s.inputMessage,
s.sendMessage,
s.addAIMessage,
s.stopGenerateMessage,
s.cancelSendMessageInServer,
chatSelectors.isAIGenerating(s),
chatSelectors.isSendButtonDisabledByMessage(s),
aiChatSelectors.isCurrentSendMessageLoading(s),
]);
const { analytics } = useAnalytics();
const checkGeminiChineseWarning = useGeminiChineseWarning();
// 使用订阅以保持最新文件列表
const reactiveFileList = useFileStore(fileChatSelectors.chatUploadFileList);
const [isUploadingFiles, clearChatUploadFileList] = useFileStore((s) => [
fileChatSelectors.isUploadingFiles(s),
s.clearChatUploadFileList,
]);
const isInputEmpty = isContentEmpty && reactiveFileList.length === 0;
const canNotSend =
isInputEmpty || isUploadingFiles || isSendButtonDisabledByMessage || isSendingMessage;
const handleSend = async (params: UseSendMessageParams = {}) => {
if (canNotSend) return;
const store = useChatStore.getState();
const mainInputEditor = store.mainInputEditor;
if (!mainInputEditor) {
console.warn('not found mainInputEditor instance');
return;
}
if (chatSelectors.isAIGenerating(store)) return;
const inputMessage = store.inputMessage;
// 发送时再取一次最新的文件列表,防止闭包拿到旧值
const fileList = fileChatSelectors.chatUploadFileList(useFileStore.getState());
// if there is no message and no image, then we should not send the message
if (!inputMessage && fileList.length === 0) return;
// Check for Chinese text warning with Gemini model
const agentStore = getAgentStoreState();
const currentModel = agentSelectors.currentAgentModel(agentStore);
const shouldContinue = await checkGeminiChineseWarning({
model: currentModel,
prompt: inputMessage,
scenario: 'chat',
});
if (!shouldContinue) return;
if (params.onlyAddAIMessage) {
addAIMessage();
} else {
sendMessage({ files: fileList, message: inputMessage, ...params });
}
clearChatUploadFileList();
mainInputEditor.setExpand(false);
mainInputEditor.clearContent();
mainInputEditor.focus();
// 获取分析数据
const userStore = getUserStoreState();
// 直接使用现有数据结构判断消息类型(支持 video)
const hasImages = fileList.some((file) => file.file?.type?.startsWith('image'));
const hasVideos = fileList.some((file) => file.file?.type?.startsWith('video'));
const messageType =
fileList.length === 0 ? 'text' : hasVideos ? 'video' : hasImages ? 'image' : 'file';
analytics?.track({
name: 'send_message',
properties: {
chat_id: store.activeId || 'unknown',
current_topic: topicSelectors.currentActiveTopic(store)?.title || null,
has_attachments: fileList.length > 0,
history_message_count: chatSelectors.activeBaseChats(store).length,
message: inputMessage,
message_length: inputMessage.length,
message_type: messageType,
selected_model: agentSelectors.currentAgentModel(agentStore),
session_id: store.activeId || 'inbox', // 当前活跃的会话ID
user_id: userStore.user?.id || 'anonymous',
},
});
};
const stop = () => {
const store = getChatStoreState();
const generating = chatSelectors.isAIGenerating(store);
if (generating) {
stopGenerateMessage();
return;
}
const isCreatingMessage = aiChatSelectors.isCurrentSendMessageLoading(store);
if (isCreatingMessage) {
cancelSendMessageInServer();
}
};
return useMemo(
() => ({
disabled: canNotSend,
generating: generating || isSendingMessage,
send: handleSend,
stop,
}),
[canNotSend, generating, isSendingMessage, stop, handleSend],
);
};
export const useSendGroupMessage = () => {
const [
isContentEmpty,
sendGroupMessage,
updateInputMessage,
stopGenerateMessage,
isSendButtonDisabledByMessage,
isCreatingMessage,
] = useChatStore((s) => [
!s.inputMessage,
s.sendGroupMessage,
s.updateInputMessage,
s.stopGenerateMessage,
chatSelectors.isSendButtonDisabledByMessage(s),
chatSelectors.isCreatingMessage(s),
]);
const isSupervisorThinking = useChatStore((s) =>
chatSelectors.isSupervisorLoading(s.activeId)(s),
);
const { analytics } = useAnalytics();
const checkGeminiChineseWarning = useGeminiChineseWarning();
const fileList = fileChatSelectors.chatUploadFileList(useFileStore.getState());
const [isUploadingFiles, clearChatUploadFileList] = useFileStore((s) => [
fileChatSelectors.isUploadingFiles(s),
s.clearChatUploadFileList,
]);
const isInputEmpty = isContentEmpty && fileList.length === 0;
const canNotSend =
isInputEmpty ||
isUploadingFiles ||
isSendButtonDisabledByMessage ||
isCreatingMessage ||
isSupervisorThinking;
const handleSend = useCallback(
async (params: UseSendGroupMessageParams = {}) => {
if (canNotSend) return;
const store = useChatStore.getState();
if (!store.activeId) return;
const mainInputEditor = store.mainInputEditor;
if (!mainInputEditor) {
console.warn('not found mainInputEditor instance');
return;
}
if (
chatSelectors.isSupervisorLoading(store.activeId)(store) ||
chatSelectors.isCreatingMessage(store)
)
return;
const inputMessage = store.inputMessage;
// if there is no message and no files, then we should not send the message
if (!inputMessage && fileList.length === 0) return;
// Check for Chinese text warning with Gemini model
const agentStore = getAgentStoreState();
const currentModel = agentSelectors.currentAgentModel(agentStore);
const shouldContinue = await checkGeminiChineseWarning({
model: currentModel,
prompt: inputMessage,
scenario: 'chat',
});
if (!shouldContinue) return;
// Append mentioned users as plain text like "@userName"
const mentionState = useMentionStore.getState();
const mentioned = mentionSelectors.mentionedUsers(mentionState);
const sessionState = useSessionStore.getState();
const mentionText =
mentioned.length > 0
? ` ${mentioned
.map((id) => sessionMetaSelectors.getAgentMetaByAgentId(id)(sessionState).title || id)
.map((name) => `@${name}`)
.join(' ')}`
: '';
const messageWithMentions = `${inputMessage}${mentionText}`.trim();
sendGroupMessage({
files: fileList,
groupId: store.activeId,
message: messageWithMentions,
targetMemberId: params.targetMemberId,
...params,
});
clearChatUploadFileList();
mainInputEditor.setExpand(false);
mainInputEditor.clearContent();
mainInputEditor.focus();
updateInputMessage('');
// clear mentioned users after sending
mentionState.clearMentionedUsers();
// 获取分析数据
const userStore = getUserStoreState();
const hasImages = fileList.some((file) => file.file?.type?.startsWith('image'));
const messageType = fileList.length === 0 ? 'text' : hasImages ? 'image' : 'file';
analytics?.track({
name: 'send_group_message',
properties: {
chat_id: store.activeId || 'unknown',
current_topic: topicSelectors.currentActiveTopic(store)?.title || null,
has_attachments: fileList.length > 0,
history_message_count: chatSelectors.activeBaseChats(store).length,
message: inputMessage,
message_length: inputMessage.length,
message_type: messageType,
selected_model: agentSelectors.currentAgentModel(agentStore),
session_id: store.activeId || 'inbox', // 当前活跃的会话ID
user_id: userStore.user?.id || 'anonymous',
},
});
},
[
canNotSend,
fileList,
clearChatUploadFileList,
updateInputMessage,
analytics,
checkGeminiChineseWarning,
],
);
const stop = useCallback(() => {
const store = getChatStoreState();
const isAgentGenerating = chatSelectors.isAIGenerating(store);
const isCreating = chatSelectors.isCreatingMessage(store);
if (isAgentGenerating) {
stopGenerateMessage();
return;
}
if (isCreating) {
// For group messages, we don't have a separate cancel method like in single chat
// The isCreatingMessage state will be reset when the operation completes
// We can potentially add a cancel group message functionality in the future
console.warn('Group message creation in progress, cannot cancel');
}
}, [stopGenerateMessage]);
return useMemo(
() => ({
disabled: canNotSend,
generating: isSupervisorThinking || isCreatingMessage,
send: handleSend,
stop,
updateInputMessage,
}),
[canNotSend, isSupervisorThinking, isCreatingMessage, handleSend, stop, updateInputMessage],
);
};